HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { ExternalLink } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { permanentRedirect } from 'next/navigation';5import { CompareButton } from '@/components/compare/compare-button';6import { CompareTrayBar } from '@/components/compare/compare-tray-bar';7import { RelationsBlock, SourcesTable, TimelineList } from '@/components/entity/blocks';8import { buildMetadata, loadEntity } from '@/components/entity/load';9import { Methodology } from '@/components/intelligence/bits';10import { ExpandableOfferRow } from '@/components/intelligence/expand-price-row';11import { ViewBeacon } from '@/components/layout/view-beacon';12import { PriceMovers } from '@/components/prices/movers';13import { ProviderStrip } from '@/components/providers/provider-strip';14import { Chip, EntityBadge, StatusBadge } from '@/components/ui/badges';15import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';16import { EntityLink, QualityMark } from '@/components/ui/entity';17import { KeyValue } from '@/components/ui/key-value';18import { SourceCell } from '@/components/ui/provenance';19import { Container, Note, Section } from '@/components/ui/section';20import { EmptyState, Unavailable } from '@/components/ui/unavailable';21import { WatchButton } from '@/components/watchlist/watch-button';22import { api, intel, safe } from '@/lib/api';23import { fmtAgo, fmtDate, fmtInt, fmtTokens, fmtUsdPerM, num } from '@/lib/format';24import { PROSE_KEYS, routes, SITE_NAME, SITE_URL } from '@/lib/site';25import type { Deployment } from '@/lib/types';2627export const revalidate = 300;28type Params = { params: Promise<{ slug: string }> };29const FEATURE_LABELS: Record<string, string> = { batch: 'Batch', cached: 'Prompt caching', fine_tuning: 'Fine-tuning', audio: 'Audio', image: 'Image', video: 'Video', web_search: 'Web search', flex: 'Flex tier', long_context: 'Long-context tier', priority: 'Priority tier' };3031export async function generateMetadata({ params }: Params): Promise<Metadata> {32 const { slug } = await params;33 const d = await safe(api.entityOfType('providers', slug));34 if (!d || d.entity_type !== 'provider') return { title: 'Provider', robots: { index: false } };35 const m = buildMetadata(d);36 return { ...m, title: `${d.name} — models served, prices per 1M tokens, price history`, description: `${d.name}${d.organization ? ` (${d.organization.name})` : ''}: every model it currently serves with published input, output, cached and batch prices per 1M tokens, context windows, price distributions, listings, delistings and price change events — each with its source. ${SITE_NAME}.` };37}3839export default async function ProviderPage({ params }: Params) {40 const { slug } = await params;41 const d = await loadEntity('providers', slug);42 const canonical = routes.entity(d);43 if (canonical !== `/providers/${encodeURIComponent(slug)}`) permanentRedirect(canonical);44 const since = Date.now() - 30 * 86400000;45 const [providers, current, closed, priceEvents] = await Promise.all([safe(intel.providers()), safe(intel.deployments({ provider: d.slug, limit: 200, sort: 'model' })), safe(intel.deployments({ provider: d.slug, current: 0, limit: 200, sort: 'valid_from' })), safe(api.changes({ type: 'PRICE_CHANGED', limit: 200 }))]);46 const row = providers?.items.find((p) => p.slug === d.slug) ?? null;47 const deployments: Deployment[] = current?.items ?? [];48 const added = deployments.filter((x) => new Date(x.valid_from).getTime() >= since).sort((a, b) => b.valid_from.localeCompare(a.valid_from));49 const removed = (closed?.items ?? []).filter((x) => x.status === 'delisted' || x.valid_to).sort((a, b) => (b.valid_to ?? '').localeCompare(a.valid_to ?? ''));50 const events = (priceEvents?.items ?? []).filter((e) => e.meta?.provider_id === d.id || e.meta?.provider === d.name);51 const orgs = [...new Map(deployments.filter((x) => x.model.organization).map((x) => [x.model.organization!.slug, x.model.organization!])).values()].sort((a, b) => a.name.localeCompare(b.name));52 const featureKeys = row?.feature_keys ?? [...new Set(deployments.flatMap((x) => Object.keys(x.features ?? {}).concat(Object.keys(x.prices.native_units ?? {}))))].sort();53 const a = d.attributes ?? {};54 const specRows = Object.keys(a)55 .filter((k) => !PROSE_KEYS.has(k))56 .map((k) => ({ key: k, raw: a[k] }));57 const ld = {58 '@context': 'https://schema.org',59 '@type': 'Organization',60 name: d.name,61 url: typeof a.website === 'string' ? a.website : undefined,62 description: d.description ?? undefined,63 parentOrganization: d.organization ? { '@type': 'Organization', name: d.organization.name } : undefined,64 mainEntityOfPage: `${SITE_URL}${canonical}`,65 };6667 return (68 <Container wide>69 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />70 <ViewBeacon path={canonical} />71 <header className="pb-5 pt-7 md:pt-10">72 <div className="flex flex-wrap items-center gap-2 text-sm text-ink-3">73 <Link href={routes.providers()} className="hover:text-ink">Providers</Link> <span aria-hidden>/</span>74 <EntityBadge type={d.entity_type} />75 <StatusBadge status={d.status} />76 </div>77 <div className="mt-2 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">78 <div className="min-w-0">79 <h1 className="display text-[28px] md:text-[40px]">{d.name}</h1>80 <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-ink-2">81 {d.organization && (82 <Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="hover:text-accent">83 Operated by {d.organization.name}84 </Link>85 )}86 {typeof a.website === 'string' && (87 <a href={a.website} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 hover:text-accent">88 {a.website.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')} <ExternalLink className="size-3" aria-hidden />89 </a>90 )}91 {typeof a.pricing_url === 'string' && (92 <a href={a.pricing_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 hover:text-accent">93 pricing page <ExternalLink className="size-3" aria-hidden />94 </a>95 )}96 <QualityMark q={d.quality?.score} label />97 </p>98 {d.description && <p className="mt-3 max-w-2xl text-[15px] leading-relaxed text-ink-2">{d.description}</p>}99 </div>100 <div className="flex shrink-0 flex-wrap items-center gap-2">101 <CompareButton e={d} />102 <WatchButton e={d} />103 <Link href={routes.graph(d.slug)} className="inline-flex h-9 items-center border border-rule px-2.5 text-sm text-ink-2 hover:border-rule-strong hover:text-ink">104 Explore graph105 </Link>106 </div>107 </div>108 </header>109110 {row ? <ProviderStrip p={row} note={providers?.note} /> : <Unavailable what="Provider aggregates" compact />}111112 {/* ---------------------------------------------------------------------------------------- models served */}113 <Section eyebrow="Models served" title={`${fmtInt(current?.total ?? deployments.length)} current deployments`} lede="One row per model × provider model id, as published. Expand a row to load its full price history as a step chart." action={{ href: `${routes.prices()}?provider=${encodeURIComponent(d.slug)}`, label: 'In the price terminal' }} hairline={false}>114 {!current ? (115 <Unavailable what="Deployments" />116 ) : deployments.length === 0 ? (117 <EmptyState title="No current deployment recorded">The pricing page has not yielded a priced model row yet.</EmptyState>118 ) : (119 <DataTable scroll compact>120 <thead>121 <tr>122 <Th>Model</Th>123 <Th>Provider model id</Th>124 <Th num>Context</Th>125 <Th num>Input / 1M</Th>126 <Th num>Output / 1M</Th>127 <Th num>Cached in</Th>128 <Th num>Batch in / out</Th>129 <Th>Status</Th>130 <Th>Observed</Th>131 <Th>Source</Th>132 <Th className="w-24" aria-label="History" />133 </tr>134 </thead>135 <tbody>136 {deployments.map((x) => (137 <ExpandableOfferRow key={x.id} model={x.model.slug} provider={d.slug} colSpan={10} mode="step">138 <Td primary>139 <EntityLink e={x.model} />140 {x.model.organization && <span className="ml-2 text-xs text-ink-3">{x.model.organization.name}</span>}141 </Td>142 <Td label="Provider model id" className="mono text-[11px] text-ink-2">{x.provider_model_id ?? '—'}</Td>143 <Td num label="Context" className="tnum text-ink-2">{fmtTokens(x.context_length)}</Td>144 <Td num label="Input / 1M" className="tnum text-accent-2">{fmtUsdPerM(x.prices.input)}</Td>145 <Td num label="Output / 1M" className="tnum text-accent-2">{fmtUsdPerM(x.prices.output)}</Td>146 <Td num label="Cached in" className="tnum text-ink-2">{fmtUsdPerM(x.prices.cached_input)}</Td>147 <Td num label="Batch" className="tnum text-ink-2">{num(x.prices.batch_input) === null && num(x.prices.batch_output) === null ? <span className="text-ink-3">—</span> : `${fmtUsdPerM(x.prices.batch_input)} / ${fmtUsdPerM(x.prices.batch_output)}`}</Td>148 <Td label="Status"><StatusBadge status={x.status} /></Td>149 <Td label="Observed" className="text-ink-2 whitespace-nowrap" title={x.observed_at}>{fmtAgo(x.observed_at)}</Td>150 <Td label="Source"><SourceCell url={x.source_url} tier={x.tier} /></Td>151 </ExpandableOfferRow>152 ))}153 {deployments.length === 0 && <EmptyRow cols={11} />}154 </tbody>155 </DataTable>156 )}157 {current && current.total > deployments.length && <Note className="mt-2">Showing the first {fmtInt(deployments.length)} of {fmtInt(current.total)} deployments — the rest are in the <Link href={`${routes.prices()}?provider=${encodeURIComponent(d.slug)}`} className="link">price terminal</Link>.</Note>}158 </Section>159160 {/* ----------------------------------------------------------------------------------- added / removed */}161 <Section eyebrow="Listings · 30 days" title="Models added and removed">162 <div className="grid grid-cols-[minmax(0,1fr)] gap-8 md:grid-cols-2">163 <div>164 <p className="flex items-baseline justify-between text-sm">165 <span className="font-medium text-ink">Added</span>166 <span className="tnum text-ink-3">{fmtInt(row?.models_added_30d ?? added.length)}</span>167 </p>168 {added.length ? (169 <ul className="mt-2 border-t border-rule">170 {added.slice(0, 12).map((x) => (171 <li key={x.id} className="flex flex-wrap items-baseline gap-x-3 border-b border-rule py-2 text-sm">172 <EntityLink e={x.model} />173 <span className="tnum ml-auto text-xs text-ink-3">{fmtDate(x.valid_from)}</span>174 </li>175 ))}176 {added.length > 12 && <li className="py-2 text-xs text-ink-3">+{added.length - 12} more in the table above.</li>}177 </ul>178 ) : (179 <p className="mt-2 text-sm text-ink-3">No listing opened in the last 30 days.</p>180 )}181 </div>182 <div>183 <p className="flex items-baseline justify-between text-sm">184 <span className="font-medium text-ink">Removed</span>185 <span className="tnum text-ink-3">{fmtInt(row?.models_removed_30d ?? removed.length)}</span>186 </p>187 {removed.length ? (188 <ul className="mt-2 border-t border-rule">189 {removed.slice(0, 12).map((x) => (190 <li key={x.id} className="flex flex-wrap items-baseline gap-x-3 border-b border-rule py-2 text-sm">191 <EntityLink e={x.model} />192 <Chip>{x.status}</Chip>193 <span className="tnum ml-auto text-xs text-ink-3">{fmtDate(x.valid_to)}</span>194 </li>195 ))}196 </ul>197 ) : (198 <p className="mt-2 text-sm text-ink-3">No delisting recorded — a model that disappears from the pricing page is closed (valid_to) and listed here.</p>199 )}200 </div>201 </div>202 </Section>203204 {/* ------------------------------------------------------------------------------------- price events */}205 <Section eyebrow="Price events" title={`Price changes · ${fmtInt(row?.price_changes_30d ?? events.length)} in 30 days`} lede="PRICE_CHANGED events where this provider is the source of the price." action={{ href: `${routes.changes()}?type=PRICE_CHANGED`, label: 'All price events' }}>206 {!priceEvents ? <Unavailable what="Price events" /> : <PriceMovers movers={events} limit={50} />}207 </Section>208209 {/* ---------------------------------------------------------------------------------- features & limits */}210 <Section eyebrow="Features & limits" title="What this provider prices and publishes">211 <div className="grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">212 <div>213 <p className="eyebrow mb-1.5">Priced features</p>214 {row?.features_supported?.length ? (215 <ul className="flex flex-wrap gap-1.5">216 {row.features_supported.map((f) => (217 <li key={f}>218 <Chip tone="accent">{FEATURE_LABELS[f] ?? f.replace(/_/g, ' ')}</Chip>219 </li>220 ))}221 </ul>222 ) : (223 <p className="text-sm text-ink-3">No priced feature beyond input/output tokens recorded.</p>224 )}225 {featureKeys.length > 0 && (226 <>227 <p className="eyebrow mb-1.5 mt-4">Native price keys observed</p>228 <ul className="mono flex flex-wrap gap-1 text-[11px] text-ink-2">229 {featureKeys.map((k) => (230 <li key={k} className="rounded-[3px] bg-surface-2 px-1.5 py-[1px]">231 {k}232 </li>233 ))}234 </ul>235 <Note className="mt-1.5">Provider-specific units (per 1K requests, per minute, per image…) are kept verbatim in each offer's native_units and are not converted.</Note>236 </>237 )}238 <p className="eyebrow mb-1.5 mt-4">Organizations covered</p>239 {orgs.length ? (240 <ul className="flex flex-wrap gap-1.5">241 {orgs.map((o) => (242 <li key={o.slug}>243 <Link href={routes.entity({ entity_type: 'company', slug: o.slug })} className="inline-flex h-7 items-center border border-rule px-2 text-xs text-ink-2 hover:border-rule-strong hover:text-ink">244 {o.name}245 </Link>246 </li>247 ))}248 </ul>249 ) : (250 <p className="text-sm text-ink-3">—</p>251 )}252 </div>253 <div>254 <p className="eyebrow mb-1.5">Published attributes</p>255 <KeyValue rows={specRows} provenance={d.provenance} slug={d.slug} entity={{ name: d.name, entity_type: d.entity_type }} dense />256 </div>257 </div>258 <Methodology text={providers?.note} />259 </Section>260261 {d.relations?.length ? (262 <Section eyebrow="Relations" title="In the graph" action={{ href: routes.graph(d.slug), label: 'Explore graph' }}>263 <RelationsBlock relations={d.relations} exclude={['available_through']} />264 </Section>265 ) : null}266 <Section eyebrow="Timeline" title="Events" action={{ href: routes.timeline({ entity: d.slug }), label: 'Full timeline' }}>267 <TimelineList events={d.timeline ?? []} slug={d.slug} />268 </Section>269 <Section eyebrow="Sources" title="Where these facts come from">270 <SourcesTable sources={d.sources ?? []} />271 </Section>272 <CompareTrayBar />273 </Container>274 );275}276